package org.microboot.core.listener;

import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.microboot.core.utils.ProcessUtil;
import org.slf4j.MDC;
import org.springframework.boot.context.config.ConfigFileApplicationListener;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.GenericApplicationListener;
import org.springframework.core.ResolvableType;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;

import java.util.Map;

/**
 * @author 胡鹏
 * <p>
 * 监听器可以按顺序监听以下事件：
 * ApplicationStartingEvent.class > ApplicationEnvironmentPreparedEvent.class >
 * ApplicationContextInitializedEvent.class > ApplicationPreparedEvent.class >
 * ApplicationStartedEvent.class > ApplicationReadyEvent.class > ApplicationFailedEvent.class
 * <p>
 * SpringBoot通过监听器和事件做了很多处理工作
 * SpringBoot启动后会在不同的位置通过多播器广播不同的事件，每次都会循环所有的监听器，但只有对应事件类型的监听器会被执行
 * SpringBoot中监听器排序序号越小，在监听器列表中越靠前，通常以Ordered.HIGHEST_PRECEDENCE或Ordered.LOWEST_PRECEDENCE为基础进行计算
 * SpringBoot中各个监听器会都有对应要处理的事件类型
 * 当我们的监听器需要在对应的事件位置执行时，最好排在相同事件对应的SpringBoot监听器之后执行
 *
 * supportsSourceType方法用于判断发布事件的事件源
 * supportsEventType方法用于判断监听的事件
 * 注意：两个方法是不一样的
 * supportsSourceType和supportsEventType都返回true时，才会调用该监听内的onApplicationEvent方法
 * supportsSourceType通常默认会直接返回true，Spring官方监听器源码默认也是返回true，即：表示可以在任意位置发布事件
 *
 * 关于事件源的说明：
 * Spring事件的顶级父类EventObject有一个成员变量Object source用来存储事件源对象
 * 每个Event类的构造方法都有事件源对象作为参数，如下：
 * 1、ApplicationStartingEvent(SpringApplication application, String[] args)：说明ApplicationStartingEvent的事件源是SpringApplication类或子类
 * 2、ContextRefreshedEvent(ApplicationContext source)：说明ContextRefreshedEvent的事件源是ApplicationContext类或子类
 * 当Spring的广播器发布事件前，需要对监听器进行筛选，筛选条件就是eventType和sourceType
 *
 * SpringBoot有两套广播器，如下：
 * 1、EventPublishingRunListener的SimpleApplicationEventMulticaster initialMulticaster
 * 2、AbstractApplicationContext的ApplicationEventMulticaster applicationEventMulticaster
 * 这两个广播器的实例对象都是SimpleApplicationEventMulticaster
 * 换句话说，如果自定义监听器的supportsSourceType默认返回true的话，那么只要eventType也符合，那么在两套广播逻辑中都会执行
 * 如果我们希望控制监听器的执行位置，除了通过supportsEventType方法绑定eventType以外，还可以通过supportsSourceType绑定sourceType
 *
 * 举例1：
 * @Override
 * public boolean supportsSourceType(Class<?> sourceType) {
 *     return sourceType.isAssignableFrom(SpringApplication.class);
 * }
 * 表示只有事件源是SpringApplication或其子类的事件，这个监听器才会生效
 *
 * 举例2：
 * @Override
 * public boolean supportsSourceType(Class<?> sourceType) {
 *     return sourceType.isAssignableFrom(ApplicationContext.class);
 * }
 * 表示只有事件源是ApplicationContext或其子类的事件，这个监听器才会生效
 *
 * 一般情况下supportsSourceType方法直接返回true就行了
 * 因为Spring和SpringBoot发布的Event是分开定义的，暂时没看到有重合
 * 而且Spring和SpringBoot各内置Event的构造方法参数默认已经绑定好事件源类型了
 * SpringApplication和ApplicationContext之间也没有继承关系
 * 但如果自定义一个事件源MySource同时继承SpringApplication，并实现ApplicationContext
 * 然后再自定义一个事件MyEvent，并绑定MySource这个事件源，
 * 最后再自定义一个监听器MyListener，绑定MySource和MyEvent，头脑风暴的案例如下：
 * public abstract class MySource extends SpringApplication implements ApplicationContext {}
 *
 * public class MyEvent extends ApplicationEvent {
 *     MyEvent(MySource mySource) {
 *         super(mySource);
 *     }
 * }
 *
 * public class MyListener implements GenericApplicationListener {
 *     @Override
 *     public boolean supportsSourceType(Class<?> sourceType) {
 *         return sourceType.isAssignableFrom(MySource.class);
 *     }
 *
 *     @Override
 *     public boolean supportsEventType(ResolvableType eventType) {
 *         return eventType.isAssignableFrom(MyEvent.class);
 *     }
 *
 *     @Override
 *     public void onApplicationEvent(ApplicationEvent event) {}
 * }
 * 如果再分别重写了SpringBoot和Spring的钩子函数，比如：
 * 1、SpringApplication的afterRefresh()方法
 * 2、AbstractApplicationContext的postProcessBeanFactory()方法
 * 然后在这两钩子函数中通过两套广播器分别发布MyEvent事件，那这个MyListener就会被执行两次
 *
 * ① 如果改一下supportsSourceType方法，如下：
 * @Override
 * public boolean supportsSourceType(Class<?> sourceType) {
 *     return sourceType.isAssignableFrom(SpringApplication.class);
 * }
 * 那么MyListener只会在SpringBoot的广播器中执行
 *
 * ② 如果改一下supportsSourceType方法，如下：
 * @Override
 * public boolean supportsSourceType(Class<?> sourceType) {
 *     return sourceType.isAssignableFrom(ApplicationContext.class);
 * }
 * 那么MyListener只会在Spring的广播器中执行
 */
public class MicrobootLoggingApplicationListener implements GenericApplicationListener {

    //本监听器需要将配置文件中日志相关配置的${pid}占位符替换成进程ID，所以需要在ConfigFileApplicationListener之后，LoggingApplicationListener之前执行
    private static final int DEFAULT_ORDER = ConfigFileApplicationListener.DEFAULT_ORDER + 1;

    private static final Class<?>[] EVENT_TYPES = {
            ApplicationEnvironmentPreparedEvent.class
    };

    /**
     * 判断发布的事件类型
     *
     * @param eventType
     * @return
     */
    @Override
    public boolean supportsEventType(ResolvableType eventType) {
        return isAssignableFrom(eventType.getRawClass(), EVENT_TYPES);
    }

    @SuppressWarnings({"unchecked"})
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        //判断事件是否是ApplicationEnvironmentPreparedEvent
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
            ConfigurableEnvironment environment = ((ApplicationEnvironmentPreparedEvent) event).getEnvironment();
            if (environment == null) {
                return;
            }
            MutablePropertySources propertySources = environment.getPropertySources();
            //log4j2配置
            for (PropertySource<?> propertySource : propertySources) {
                Object source = propertySource.getSource();
                if (!(source instanceof Map)) {
                    continue;
                }
                Map<String, Object> map = (Map<String, Object>) source;
                for (String key : map.keySet()) {
                    if (!StringUtils.contains(key, "microboot.logging")) {
                        continue;
                    }
                    String value = MapUtils.getString(map, key);
                    if (StringUtils.isBlank(value)) {
                        continue;
                    }
                    String newKey = StringUtils.replace(key, "microboot.logging.", "");
                    if (StringUtils.containsIgnoreCase(value, "${pid}")) {
                        value = StringUtils.replace(value, "${pid}", ProcessUtil.getProcessId());
                    }
                    MDC.put(newKey, value);
                }
            }
        }
    }

    @Override
    public int getOrder() {
        return DEFAULT_ORDER;
    }

    private boolean isAssignableFrom(Class<?> type, Class<?>... supportedTypes) {
        if (type != null) {
            for (Class<?> supportedType : supportedTypes) {
                if (supportedType.isAssignableFrom(type)) {
                    return true;
                }
            }
        }
        return false;
    }
}
